<?php
/**
 * Zend Framework
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 * @category Zend
 * @package Zend_Paginator
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 * @version $Id: DbSelect.php 24754 2012-05-05 02:30:56Z adamlundrigan $
 */
/**
 *
 * @see Zend_Paginator_Adapter_Interface
 */
require_once 'Zend/Paginator/Adapter/Interface.php';
/**
 *
 * @see Zend_Db
 */
require_once 'Zend/Db.php';
/**
 *
 * @see Zend_Db_Select
 */
require_once 'Zend/Db/Select.php';

/**
 *
 * @category Zend
 * @package Zend_Paginator
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
class Zend_Paginator_Adapter_DbSelect implements Zend_Paginator_Adapter_Interface {

    /**
     * Name of the row count column
     * @var string
     */
    const ROW_COUNT_COLUMN = 'zend_paginator_row_count';

    /**
     * The COUNT query
     * @var Zend_Db_Select
     */
    protected $_countSelect = null;

    /**
     * Database query
     * @var Zend_Db_Select
     */
    protected $_select = null;

    /**
     * Total item count
     * @var integer
     */
    protected $_rowCount = null;

    /**
     * Identifies this adapter for caching purposes.
     * This value will remain constant for
     * the entire life of this adapter regardless of how many different pages are queried.
     * @var string
     */
    protected $_cacheIdentifier = null;

    /**
     * Constructor.
     * @param Zend_Db_Select $select The select query
     */
    public function __construct (Zend_Db_Select $select) {
        $this -> _select = $select;
        $this -> _cacheIdentifier = md5 ($select -> assemble ());
    }

    /**
     * Returns the cache identifier.
     * @return string
     */
    public function getCacheIdentifier () {
        return $this -> _cacheIdentifier;
    }

    /**
     * Sets the total row count, either directly or through a supplied
     * query.
     * Without setting this, {@link getPages()} selects the count
     * as a subquery (SELECT COUNT ... FROM (SELECT ...)). While this
     * yields an accurate count even with queries containing clauses like
     * LIMIT, it can be slow in some circumstances. For example, in MySQL,
     * subqueries are generally slow when using the InnoDB storage engine.
     * Users are therefore encouraged to profile their queries to find
     * the solution that best meets their needs.
     * @param Zend_Db_Select|integer $totalRowCount Total row count integer
     *        or query
     * @return Zend_Paginator_Adapter_DbSelect $this
     * @throws Zend_Paginator_Exception
     */
    public function setRowCount ($rowCount) {
        if ($rowCount instanceof Zend_Db_Select) {
            $columns = $rowCount -> getPart (Zend_Db_Select::COLUMNS);
            $countColumnPart = empty ($columns[0][2]) ? $columns[0][1] : $columns[0][2];
            if ($countColumnPart instanceof Zend_Db_Expr) {
                $countColumnPart = $countColumnPart -> __toString ();
            }
            $rowCountColumn = $this -> _select -> getAdapter () -> foldCase (self::ROW_COUNT_COLUMN);
            // The select query can contain only one column, which should be the row count column
            if (false === strpos ($countColumnPart, $rowCountColumn)) {
                /**
                 *
                 * @see Zend_Paginator_Exception
                 */
                require_once 'Zend/Paginator/Exception.php';
                throw new Zend_Paginator_Exception ('Row count column not found');
            }
            $result = $rowCount -> query (Zend_Db::FETCH_ASSOC) -> fetch ();
            $this -> _rowCount = count ($result) > 0 ? $result[$rowCountColumn] : 0;
        } else 
            if (is_integer ($rowCount)) {
                $this -> _rowCount = $rowCount;
            } else {
                /**
                 *
                 * @see Zend_Paginator_Exception
                 */
                require_once 'Zend/Paginator/Exception.php';
                throw new Zend_Paginator_Exception ('Invalid row count');
            }
        return $this;
    }

    /**
     * Returns an array of items for a page.
     * @param integer $offset Page offset
     * @param integer $itemCountPerPage Number of items per page
     * @return array
     */
    public function getItems ($offset, $itemCountPerPage) {
        $this -> _select -> limit ($itemCountPerPage, $offset);
        return $this -> _select -> query () -> fetchAll ();
    }

    /**
     * Returns the total number of rows in the result set.
     * @return integer
     */
    public function count () {
        if ($this -> _rowCount === null) {
            $this -> setRowCount ($this -> getCountSelect ());
        }
        return $this -> _rowCount;
    }

    /**
     * Get the COUNT select object for the provided query
     * TODO: Have a look at queries that have both GROUP BY and DISTINCT specified.
     * In that use-case I'm expecting problems when either GROUP BY or DISTINCT
     * has one column.
     * @return Zend_Db_Select
     */
    public function getCountSelect () {
        /**
         * We only need to generate a COUNT query once.
         * It will not change for
         * this instance.
         */
        if ($this -> _countSelect !== null) {
            return $this -> _countSelect;
        }
        $rowCount = clone $this -> _select;
        $rowCount -> __toString (); // Workaround for ZF-3719 and related
        $db = $rowCount -> getAdapter ();
        $countColumn = $db -> quoteIdentifier ($db -> foldCase (self::ROW_COUNT_COLUMN));
        $countPart = 'COUNT(1) AS ';
        $groupPart = null;
        $unionParts = $rowCount -> getPart (Zend_Db_Select::UNION);
        /**
         * If we're dealing with a UNION query, execute the UNION as a subquery
         * to the COUNT query.
         */
        if ( ! empty ($unionParts)) {
            $expression = new Zend_Db_Expr ($countPart . $countColumn);
            $rowCount = $db -> select () -> bind ($rowCount -> getBind ()) -> from ($rowCount, $expression);
        } else {
            $columnParts = $rowCount -> getPart (Zend_Db_Select::COLUMNS);
            $groupParts = $rowCount -> getPart (Zend_Db_Select::GROUP);
            $havingParts = $rowCount -> getPart (Zend_Db_Select::HAVING);
            $isDistinct = $rowCount -> getPart (Zend_Db_Select::DISTINCT);
            /**
             * If there is more than one column AND it's a DISTINCT query, more
             * than one group, or if the query has a HAVING clause, then take
             * the original query and use it as a subquery os the COUNT query.
             */
            if (($isDistinct && ((count ($columnParts) == 1 && $columnParts[0][1] == Zend_Db_Select::SQL_WILDCARD) || count ($columnParts) > 1)) || count ($groupParts) > 1 ||  ! empty ($havingParts)) {
                $rowCount -> reset (Zend_Db_Select::ORDER);
                $rowCount = $db -> select () -> bind ($rowCount -> getBind ()) -> from ($rowCount);
            } else 
                if ($isDistinct) {
                    $part = $columnParts[0];
                    if ($part[1] !== Zend_Db_Select::SQL_WILDCARD &&  ! ($part[1] instanceof Zend_Db_Expr)) {
                        $column = $db -> quoteIdentifier ($part[1], true);
                        if ( ! empty ($part[0])) {
                            $column = $db -> quoteIdentifier ($part[0], true) . '.' . $column;
                        }
                        $groupPart = $column;
                    }
                } else 
                    if ( ! empty ($groupParts)) {
                        $groupPart = $db -> quoteIdentifier ($groupParts[0], true);
                    }
            /**
             * If the original query had a GROUP BY or a DISTINCT part and only
             * one column was specified, create a COUNT(DISTINCT ) query instead
             * of a regular COUNT query.
             */
            if ( ! empty ($groupPart)) {
                $countPart = 'COUNT(DISTINCT ' . $groupPart . ') AS ';
            }
            /**
             * Create the COUNT part of the query
             */
            $expression = new Zend_Db_Expr ($countPart . $countColumn);
            $rowCount -> reset (Zend_Db_Select::COLUMNS) -> reset (Zend_Db_Select::ORDER) -> reset (Zend_Db_Select::LIMIT_OFFSET) -> reset (Zend_Db_Select::GROUP) -> reset (Zend_Db_Select::DISTINCT) -> reset (Zend_Db_Select::HAVING) -> columns ($expression);
        }
        $this -> _countSelect = $rowCount;
        return $rowCount;
    }

}
